﻿//-----------------------------------------------------------------------
// <copyright file="ServerNetwork.cs" company="FastNET Services">
//     Copyright (c) 2008-2009. All Rights Reserved.
// </copyright>
// <license>
//     GPLv2
// </license>
// <warranty>
//     THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
//     BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
//     NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
//     DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//     OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// </warranty>
//-----------------------------------------------------------------------
#region Usings

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Security.Cryptography;
using GateShift.Common.Networking.Enumerations;
using GateShift.Common.Networking.Events;
using GateShift.Common.Networking.Structures;
using GateShift.Servers.GameServer.Networking.Events;
using GateShift.Services.SDK.Threading;
using ICSharpCode.SharpZipLib.Core;
using ICSharpCode.SharpZipLib.GZip;

#endregion

namespace GateShift.Servers.GameServer.Networking
{
    /// <summary>
    /// Standard Server Networking Management Class
    /// </summary>
    public class ServerNetwork
    {
        #region Variables

        private readonly ThreadedDictionary<Guid, ServerSocket> _connections;
        private readonly Socket _listener;
        private readonly object _acceptLock;
        private SymmetricAlgorithm _encryption;
        private bool _currentlyAccepting;

        #endregion

        #region CTor/DTor

        /// <summary>
        /// Initializes a new instance of the ServerNetwork class
        /// </summary>
        /// <param name="isIPv6">Boolean whether or not this instance is binding to IPv4 or IPv6</param>
        public ServerNetwork(bool isIPv6)
        {
            _connections = new ThreadedDictionary<Guid, ServerSocket>();
            _listener = new Socket((isIPv6 ? AddressFamily.InterNetworkV6 : AddressFamily.InterNetwork), SocketType.Stream, ProtocolType.Tcp);
            _currentlyAccepting = false;
            _acceptLock = new object();
        }

        #endregion

        #region Events

        /// <summary>
        /// Event fired when a complete packet has been recieved
        /// </summary>
        public event EventHandler<ServerDataRecievedEventArgs> DataRecieved;

        /// <summary>
        /// Event fired when a client has connected
        /// </summary>
        public event EventHandler<ConnectionIDEventArgs> ClientConnected;

        /// <summary>
        /// Event fired when a client has disconnected
        /// </summary>
        public event EventHandler<ConnectionIDEventArgs> ClientDisconnected;

        #endregion

        #region Properties

        /// <summary>
        /// Gets or sets the maximum number of Queued connections to allow at any given time.
        /// </summary>
        public int MaximumWaitingConnections { get; set; }

        /// <summary>
        /// Gets or sets the maximum number of Total connections to accept at any given time.
        /// </summary>
        public int MaximumTotalConnections { get; set; }

        /// <summary>
        /// Gets or sets a value indicating whether or not to Compress all Outbound traffic
        /// </summary>
        public bool CompressOutboundTraffic { get; set; }

        /// <summary>
        /// Gets a value indicating whether or not the current instance is bound or not
        /// </summary>
        public bool IsBound
        {
            get
            {
                return _listener.IsBound;
            }
        }

        #endregion

        #region Public Methods

        #region Binding

        /// <summary>
        /// Binds the listener to a specified IPEndPoint
        /// </summary>
        /// <param name="localAddress">IPEndPoint to bind to. Must match type from constructor.</param>
        public void Bind(IPEndPoint localAddress)
        {
            //// if (_listener.IsBound)
            //// throw new InvalidOperationException("Cannot rebind with this instance of the class.");

            _listener.Bind(localAddress);
            _listener.Listen(MaximumWaitingConnections);
        }

        #endregion

        #region Disconnecting Methods

        /// <summary>
        /// Disconnects the specified connection ID
        /// </summary>
        /// <param name="connectionID">Guid of the connection to disconnect</param>
        public void Disconnect(Guid connectionID)
        {
            // Make sure the connection exists
            if (!_connections.ContainsKey(connectionID))
            {
                throw new KeyNotFoundException("Connection does not exist or may have already been disconnected.");
            }

            // Tell the ServerSocket to disconnect
            _connections[connectionID].Disconnect();
        }

        #endregion

        #region Encryption/Decryption

        /// <summary>
        /// Sets the algorithm to encrypt by based on the configuration section in the config file.
        /// This can be a default name from .NET types or a custom implementation.
        /// </summary>
        /// <param name="algorithmName">Config name from http://msdn.microsoft.com/en-us/library/system.security.cryptography.cryptoconfig.aspx</param>
        /// <param name="key">Encryption Key to use</param>
        /// <param name="iv">Encryption Initialization Vector to use</param>
        public void SetEncryption(string algorithmName, byte[] key, byte[] iv)
        {
            if (_encryption != null)
            {
                _encryption.Clear();
            }

            _encryption = SymmetricAlgorithm.Create(algorithmName);

            _encryption.Padding = PaddingMode.ISO10126;
            _encryption.Key = key;
            _encryption.IV = iv;
        }

        #endregion

        #region Sending Data

        /// <summary>
        /// Sends data to the specified connectionID
        /// </summary>
        /// <param name="connectionID">Guid ID for the client connected that will recieve data</param>
        /// <param name="serviceID">Service ID to notify</param>
        /// <param name="serviceCode">Code for the Service ID to handle</param>
        public void Send(Guid connectionID, byte serviceID, byte serviceCode)
        {
            Send(connectionID, serviceID, serviceCode, null, false);
        }

        /// <summary>
        /// Sends data to the specified connectionID
        /// </summary>
        /// <param name="connectionID">Guid ID for the client connected that will recieve data</param>
        /// <param name="serviceID">Service ID to notify</param>
        /// <param name="serviceCode">Code for the Service ID to handle</param>
        /// <param name="data">Data to send to client. Can be NULL</param>
        public void SendData(Guid connectionID, byte serviceID, byte serviceCode, byte[] data)
        {
            Send(connectionID, serviceID, serviceCode, data, false);
        }

        /// <summary>
        /// Sends data to the specified connectionID
        /// </summary>
        /// <param name="connectionID">Guid ID for the client connected that will recieve data</param>
        /// <param name="serviceID">Service ID to notify</param>
        /// <param name="serviceCode">Code for the Service ID to handle</param>
        /// <param name="data">Data to send to client. Can be NULL only if encryptData is false</param>
        /// <param name="encryptData">Boolean whether or not to encrypt data. Can only set to true if data is NOT NULL.</param>
        public void Send(Guid connectionID, byte serviceID, byte serviceCode, byte[] data, bool encryptData)
        {
            Send(connectionID, serviceID, serviceCode, data, false, encryptData);
        }

        #endregion

        #region Accept Connections

        /// <summary>
        /// Start accepting incoming connections
        /// </summary>
        public void StartAccepting()
        {
            // If we have our maximum connection set, return until a disconnect
            if (_connections.Count >= MaximumTotalConnections)
            {
                return;
            }

            // Mark that we are starting to accept
            lock (_acceptLock)
            {
                if (_currentlyAccepting)
                {
                    return;
                }

                _currentlyAccepting = true;
            }

            // Try to start accepting
            try
            {
                _listener.BeginAccept(AcceptClient, null);
            }
            catch (SocketException)
            {
                // TODO: What Errors could happen here?
            }
        }

        #endregion

        #endregion

        #region Event Raisers

        /// <summary>
        /// Calls the ClientConnected event in a thread safe manner
        /// </summary>
        /// <param name="e">ConnectionIDEventArgs with information about the event</param>
        protected void OnClientConnected(ConnectionIDEventArgs e)
        {
            if (ClientConnected == null)
            {
                return;
            }

            // Try a safe invoke for calls from a different thread
            ISynchronizeInvoke safeInvoker = ClientConnected.Target as ISynchronizeInvoke;

            if (safeInvoker != null)
            {
                safeInvoker.Invoke(ClientConnected, new object[] { this, e });
            }
            else
            {
                ClientConnected.Invoke(this, e);
            }
        }

        /// <summary>
        /// Calls the ClientDisconnected event in a thread safe manner
        /// </summary>
        /// <param name="e">ConnectionIDEventArgs with information about the event</param>
        protected void OnClientDisconnected(ConnectionIDEventArgs e)
        {
            if (ClientDisconnected == null)
            {
                return;
            }

            // Try a safe invoke for calls from a different thread
            ISynchronizeInvoke safeInvoker = ClientDisconnected.Target as ISynchronizeInvoke;

            if (safeInvoker != null)
            {
                safeInvoker.Invoke(ClientDisconnected, new object[] { this, e });
            }
            else
            {
                ClientDisconnected.Invoke(this, e);
            }
        }

        /// <summary>
        /// Calls the DataRecieved event in a thread safe manner
        /// </summary>
        /// <param name="e">ServerDataRecievedEventArgs with information about the event</param>
        protected void OnDataRecieved(ServerDataRecievedEventArgs e)
        {
            if (DataRecieved == null)
            {
                return;
            }

            // Try a safe invoke for calls from a different thread
            ISynchronizeInvoke safeInvoker = DataRecieved.Target as ISynchronizeInvoke;

            if (safeInvoker != null)
            {
                safeInvoker.Invoke(DataRecieved, new object[] { this, e });
            }
            else
            {
                DataRecieved.Invoke(this, e);
            }
        }

        #endregion

        #region Packet Modification

        /// <summary>
        /// Mdifies packets going to the server based on the flags
        /// </summary>
        /// <param name="packet">DataPacket to modify</param>
        /// <returns>Modified DataPacket</returns>
        private DataPacket PreparePacket(DataPacket packet)
        {
            // Return the packet as is when there is no data
            if (packet.Data == null)
            {
                return packet;
            }

            // Do we need to encrypt?
            if (((packet.Flags | DataPacketFlags.Encrypted) == packet.Flags) && _encryption != null)
            {
                // Create Streams
                MemoryStream baseStream = new MemoryStream();
                MemoryStream sourceStream = new MemoryStream(packet.Data);
                CryptoStream destinationStream = new CryptoStream(baseStream, _encryption.CreateEncryptor(), CryptoStreamMode.Write);

                // Transform Data
                StreamUtils.Copy(sourceStream, destinationStream, new byte[4096]);
                destinationStream.FlushFinalBlock();

                // Set transformed data
                packet.Data = baseStream.ToArray();

                // Close Streams
                sourceStream.Close();
                destinationStream.Close();
                baseStream.Close();
            }

            // Do we need to compress?
            if ((packet.Flags | DataPacketFlags.Compressed) == packet.Flags)
            {
                // Create Streams
                MemoryStream baseStream = new MemoryStream();
                MemoryStream sourceStream = new MemoryStream(packet.Data);
                GZipOutputStream destinationStream = new GZipOutputStream(baseStream);

                // Transform Data
                StreamUtils.Copy(sourceStream, destinationStream, new byte[4096]);
                destinationStream.Finish();

                // Set transformed data
                packet.Data = baseStream.ToArray();

                // Close Streams
                sourceStream.Close();
                destinationStream.Close();
                baseStream.Close();
            }

            return packet;
        }

        /// <summary>
        /// Unmodifies packets coming back from the server based on the flags
        /// </summary>
        /// <param name="packet">DataPacket to unmodify</param>
        /// <returns>Unmodified DataPacket</returns>
        private DataPacket UnpreparePacket(DataPacket packet)
        {
            // Return the packet as is when there is no data
            if (packet.Data == null)
            {
                return packet;
            }

            // Do we need to decompress?
            if ((packet.Flags | DataPacketFlags.Compressed) == packet.Flags)
            {
                // Create Streams
                MemoryStream baseStream = new MemoryStream(packet.Data);
                MemoryStream destinationStream = new MemoryStream();
                GZipInputStream sourceStream = new GZipInputStream(baseStream);

                // Transform Data
                StreamUtils.Copy(sourceStream, destinationStream, new byte[4096]);

                // Set transformed data
                packet.Data = destinationStream.ToArray();

                // Close Streams
                sourceStream.Close();
                destinationStream.Close();
                baseStream.Close();
            }

            // Do we need to encrypt?
            if (((packet.Flags | DataPacketFlags.Encrypted) == packet.Flags) && _encryption != null)
            {
                // Create Streams
                MemoryStream baseStream = new MemoryStream(packet.Data);
                MemoryStream destinationStream = new MemoryStream();
                CryptoStream sourceStream = new CryptoStream(baseStream, _encryption.CreateDecryptor(), CryptoStreamMode.Read);

                // Transform Data
                StreamUtils.Copy(sourceStream, destinationStream, new byte[4096]);

                // Set transformed data
                packet.Data = destinationStream.ToArray();

                // Close Streams
                sourceStream.Close();
                destinationStream.Close();
                baseStream.Close();
            }

            return packet;
        }

        #endregion

        #region Sending Data

        /// <summary>
        /// Sends data to the specified connectionID
        /// </summary>
        /// <param name="connectionID">Guid ID for the client connected that will recieve data</param>
        /// <param name="serviceID">Service ID to notify</param>
        /// <param name="serviceCode">Code for the Service ID to handle</param>
        /// <param name="data">Data to send to client. Can be NULL only if encryptData is false</param>
        /// <param name="connectionRelated">Boolean whether or not the data is internal to the network library</param>
        /// <param name="encryptData">Boolean whether or not to encrypt data. Can only set to true if data is NOT NULL.</param>
        private void Send(Guid connectionID, byte serviceID, byte serviceCode, byte[] data, bool connectionRelated, bool encryptData)
        {
            DataPacket packet = new DataPacket
                                    {
                                        PrimaryCode = serviceID,
                                        SecondaryCode = serviceCode,
                                        Flags = DataPacketFlags.None,
                                        Data = data
                                    };

            // Handle Flags
            if (connectionRelated)
            {
                packet.Flags |= DataPacketFlags.ConnectionRelated;
            }

            if (encryptData)
            {
                packet.Flags |= DataPacketFlags.Encrypted;
            }

            if (CompressOutboundTraffic)
            {
                packet.Flags |= DataPacketFlags.Compressed;
            }

            // Validations
            if (encryptData && data == null)
            {
                throw new ArgumentNullException("data", "Data cannot be null if encryptData is specified");
            }

            // Do we need to handle data?
            if (data != null)
            {
                // Compress/Encrypt packet
                packet = PreparePacket(packet);

                // Set the size after modifications
                packet.DataSize = packet.Data.Length;
            }

            // Check for connection
            if (!_connections.ContainsKey(connectionID))
            {
                throw new KeyNotFoundException("Connection does not exist or may have already been disconnected.");
            }

            // Tell ServerSocket to send packet
            _connections[connectionID].SendPacket(packet);
        }

        #endregion

        #region ServerSocket Events

        /// <summary>
        /// Event handler for a packet being recieved
        /// </summary>
        /// <param name="sender">ServerSocket with the packet recieved</param>
        /// <param name="e">PacketRecievedEventArgs with information about the Event</param>
        private void ServerSocket_PacketRecieved(object sender, PacketRecievedEventArgs e)
        {
            // Get information from event arguments
            ServerSocket connection = sender as ServerSocket;
            DataPacket packet = e.Packet;

            // Make sure we got a cast
            if (connection == null)
            {
                return;
            }

            // Unencrypt and uncompress packet
            packet = UnpreparePacket(packet);

            // If the packet is an internal packet, handle it without letting the world know we got it
            if ((packet.Flags & DataPacketFlags.ConnectionRelated) == DataPacketFlags.ConnectionRelated)
            {
                HandlePrivateData(packet);
            }
            else
            {
                OnDataRecieved(new ServerDataRecievedEventArgs(connection.ConnectionID, packet));
            }
        }

        /// <summary>
        /// Event handler for a disconnecting client
        /// </summary>
        /// <param name="sender">ServerSocket being disconnected</param>
        /// <param name="e">Empty EventArgs</param>
        private void ServerSocket_Disconnected(object sender, EventArgs e)
        {
            ServerSocket connection = sender as ServerSocket;

            // Make sure the cast is correct
            if (connection != null)
            {
                connection.Disconnected -= ServerSocket_Disconnected;
                connection.PacketRecieved -= ServerSocket_PacketRecieved;

                _connections.Remove(connection.ConnectionID);
                OnClientDisconnected(new ConnectionIDEventArgs(connection.ConnectionID));
            }

            // Since we have a disconnected client, try to accept another
            StartAccepting();
        }

        #endregion

        #region Accept Connections

        /// <summary>
        /// Callback for accepting a new client connection
        /// </summary>
        /// <param name="result">IAsyncResult with information from the callback</param>
        private void AcceptClient(IAsyncResult result)
        {
            Socket newSocket = null;

            // Try to accept the client
            try
            {
                newSocket = _listener.EndAccept(result);
            }
            catch (SocketException)
            {
                // TODO: What Errors could happen here?
            }

            // If we have a client, then handle it
            if (newSocket != null)
            {
                // Make a connection using our helper class
                ServerSocket connection = new ServerSocket(newSocket);

                // Attach events
                connection.Disconnected += ServerSocket_Disconnected;
                connection.PacketRecieved += ServerSocket_PacketRecieved;

                // Add to our list of connections
                _connections.Add(connection.ConnectionID, connection);

                // Notify we have a client connected
                OnClientConnected(new ConnectionIDEventArgs(connection.ConnectionID));
            }

            // Mark that we have finished accepting the client
            lock (_acceptLock)
            {
                _currentlyAccepting = false;
            }

            // Start accepting another client
            StartAccepting();
        }

        #endregion

        #region Handle Private Packets

        /// <summary>
        /// Handles connection  related packets for the current connection
        /// </summary>
        /// <param name="packet">DataPacket to handle</param>
        private void HandlePrivateData(DataPacket packet)
        {
            switch (packet.PrimaryCode)
            {
                default:
                    return;
            }
        }

        #endregion
    }
}